/*
* MIDIMusic - Play music through MIDI according to the code file given.
* (c) apple-ba 2024. All rights reserved.
*/

// Includes.
#include <chrono>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <Windows.h>

// Libraries.
#pragma comment(lib, "winmm.lib")

// Namespace.
using namespace std;
using namespace std::chrono;

// Global variables.
long long StartTime = -1;	// When did this program start.
HMIDIOUT handle;			// The MIDI handle.
bool NoColor = false;		// Use output text color?
bool ShowLog = true;		// Show log?
bool File = false;			// Use file?
string InputFile;			// If input file used, the input file name.
ifstream in;				// If input file used, the ifstream.

// Macros.

// Colors.
#define GREEN FOREGROUND_GREEN	
#define BLUE FOREGROUND_BLUE	
#define RED FOREGROUND_RED	

// Version.
#ifdef _DEBUG
#ifdef _M_AMD64
#define Version (string("X64 DEBUG ") + __DATE__ + " " + __TIME__).c_str()
#endif // _M_AMD64
#ifdef _M_IX86_FP
#define Version (string("X86 DEBUG ") + __DATE__ + " " + __TIME__).c_str()
#endif // _M_IX86_FP
#else
#ifdef _M_AMD64
#define Version (string("X64 RELEASE ") + __DATE__).c_str()
#endif // _M_AMD64
#ifdef _M_IX86_FP
#define Version (string("X86 RELEASE ") + __DATE__).c_str()
#endif // _M_IX86_FP
#endif // _DEBUG

// Lowest and Highest code file version supported.
#define LowestSupported 1
#define HighestSupported 2

// Maximum Threads.
#define MAX_THREADS 16

// Sleep until X. Do nothing if X is before the time now.
#define SleepTill(x) Sleep(DWORD(max((x) - duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(), 0))), LastStop = (long long)(x)

// Print log Y in color X.
#define PrintLog(x, y, ...)\
if(ShowLog)\
{\
	SetColor(x);\
	printf_s((string("%.10lld [Thread%2d] ") + y + "\n").c_str(), duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(), Index, __VA_ARGS__);\
}

// Functions.

// Set output text color
static void SetColor(WORD foreground = 7)
{
	if (NoColor) return;
	HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	GetConsoleScreenBufferInfo(handle_out, &csbi);
	SetConsoleTextAttribute(handle_out, foreground);
}

// End the program, returning X.
static void End(int x)
{
	SetColor();
	printf_s("Press any key to continue...");
	system("pause > nul");
	printf_s("\n");
	exit(x);
}

static int SyncCount[MAX_THREADS] = {};
int threads = 0;

// The thread function.
static DWORD WINAPI Play(LPVOID lp)
{
	// Variables.
	int Index = (int*)lp == 0 ? 0 : *(int*)lp;		// Which thread am I?
	ifstream nin;									// The new ifstream.
	string s;										// The string from the input.
	int LineNumber = 0;								// The input line.
	int C = 12;										// The tone.
	double p = 750;									// The speed. (1/4 = ?)
	bool link = false;								// Link(~): less stop between the two sounds.
	bool quiet = false;								// Quiet(()): not so loud.
	bool MultiSound = false;						// Multisound({}): Play several sounds at a time.
	bool NoStop = false;							// NoStop: Do not use 0x84 MIDI code.
	vector<int> Sounds;								// The sounds to play.
	double t = 0;									// Time of the sound.
	short strength = 64;							// Strength of the sound.

	// Initialize: Wait till 1.5s after StartTime. That ensures all threads start at nearly the same time.
	long long LastStop = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
	SleepTill(StartTime + 5000);
	PrintLog(GREEN, "Recall: Thread initialized.");

	// I'm reading code from a file, do some preparations.
	if (File)
	{
		// Open the input file.
		while (true)
		{
			// Try to open.
			nin.open(InputFile);

			// If failed.
			if (!nin.is_open())
			{
				char errstr[1024];
				strerror_s(errstr, errno);
				SetColor(RED);
				printf_s("SYSTEM ERROR: Cannot open input file %s: %s\n", InputFile.c_str(), errstr);
				SetColor(GREEN | BLUE);
				char c = 0;
				while (true)
				{
					printf_s("Retry? [Y/N]");
					cin >> c;
					if (c == 'Y' || c == 'y')
						break;
					else if (c == 'N' || c == 'n')
						End(1);
				}
			}
			else break;
		}

		// I'm not the only thread, do some preparations.
		if (Index != 0)
		{
			//Seek for my Thread mark.
			stringstream ss;
			ss << "Thread" << Index << ":";
			string target;
			ss >> target;
			string s;
			while (getline(nin, s))
			{
				LineNumber++;
				if (s.find(target) != s.npos && s.find(target) < s.find(";"))
				{
					break;
				}
			}
		}

	}
	// Play.

	// 'Target' is when I should stop.
	string target;
	stringstream ss;
	ss << "Thread" << Index + 1 << ":";
	ss >> target;

	if (!File)
	{
		SetColor();
		printf_s(">");
	}

	// Always input, from the console, the public ifstream, or my own ifstream.
	while (getline(File ? nin : cin, s))
	{
		// That's where I should stop.
		if (s.find(target) != s.npos && s.find(target) < s.find(";"))
		{
			PrintLog(GREEN, "Recall: Thread end.");
			return 0;
		}

		// Reset LastStop if in interact mode.
		if (!File)
		{
			LastStop = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
		}

		// Print what I've got from the file. 
		if (File)
		{
			PrintLog(GREEN | BLUE, " Input: %s", s.c_str());
		}

		// Add my LineNumber.
		LineNumber++;

		// Reset my time counter.
		if (!MultiSound)
		{
			t = 0;
		}

		// Skip the comment line.
		if (s[0] == ';')
		{
			s = "";
		}

		// Skip the Version note line.
		if (s.find("Music code file Version ") != s.npos)
		{
			s = "";
		}

		if (s.find("Instrument=") != s.npos)
		{
			s.erase(0, 11);
			//s.erase(0, s.find_first_of('='));
			stringstream sss;
			sss << s;
			int InstrumentNo;
			sss >> InstrumentNo;
			int voice = (InstrumentNo << 8) + 0xC0 + (Index == 0 ? 0 : Index - 1);
			PrintLog(RED | GREEN, "Recall: SET MUSICAL INSTRUMENT (MIDI: %.8X)", voice);
			midiOutShortMsg(handle, voice);
			s = "";
		}

		if (s.find("Strength=") != s.npos)
		{
			s.erase(0, 9);
			strength = atoi(s.c_str());
			PrintLog(BLUE, "Recall: Set Strength = %.8X", strength);
			s = "";
		}

		// The code.
		while (s.size() != 0)
		{
			// Variables.
			string ss;		// The sound.

			// Separate: Find the space and slide.
			size_t fspace = s.find_first_of(' '), fslide = s.find_first_of('~');
			if (fspace == s.npos && fslide == s.npos)
			{
				ss = s;
				s = "";
			}
			else if (fspace == s.npos)
			{
				ss = s.substr(0, fslide);
				s.erase(0, fslide);
				s.erase(0, s.find_first_not_of('~'));
				link = true;
			}
			else if (fslide == s.npos)
			{
				ss = s.substr(0, fspace);
				s.erase(0, fspace);
				s.erase(0, s.find_first_not_of(' '));
			}
			else
			{
				if (fspace < fslide)
				{
					ss = s.substr(0, fspace);
					s.erase(0, fspace);
					s.erase(0, s.find_first_not_of(' '));
				}
				else
				{
					ss = s.substr(0, fslide);
					s.erase(0, fslide);
					s.erase(0, s.find_first_not_of('~'));
					link = true;
				}

			}

			// NoStop enabled.
			if (ss == "[")
			{
				NoStop = true;
				PrintLog(BLUE, "Recall: NoStop enabled.");
			}
			// NoStop disabled.
			else if (ss == "]")
			{
				NoStop = false;
				PrintLog(BLUE, "Recall: NoStop disabled.");
			}

			else if (ss == "|")
			{
				SyncCount[Index]++;
				bool flag = true;
				while (flag)
				{
					flag = false;
					for (int i = 1; i <= threads; i++)
					{
						if (SyncCount[Index] > SyncCount[i])
						{
							flag = true;
							SleepTill(LastStop + 1);
							break;
						}
					}
				}
			}

			// Quiet enabled.
			else if (ss == "(")
			{
				quiet = true;
				PrintLog(BLUE, "Recall: Quiet enabled.");
			}
			// Quiet disabled.
			else if (ss == ")")
			{
				quiet = false;
				PrintLog(BLUE, "Recall: Quiet disabled.");
			}

			// MultiSound mark.
			else if (ss == "{")
			{
				MultiSound = true;
				PrintLog(BLUE, "Recall: MultiSound start.");
			}
			//MultiSound end.
			else if (ss == "}")
			{
				// Set the flag.
				MultiSound = false;

				// Play the sounds.
				for (auto i : Sounds)
				{
					int voice = (int((quiet ? 0.5 : 1.0) * strength) << 16) + (i << 8) + 0x90 + (Index == 0 ? 0 : Index - 1);
					PrintLog(RED | GREEN, "Recall: START (MIDI: %.8X)", voice);
					midiOutShortMsg(handle, voice);
				}

				// Wait.
				SleepTill(LastStop + t - p / (link ? 64 : 32));

				// Stop the sounds.
				if (!NoStop)
				{
					for (auto i : Sounds)
					{
						int voice = (i << 8) + 0x80 + (Index == 0 ? 0 : Index - 1);
						PrintLog(RED | GREEN, "Recall: STOP  (MIDI: %.8X)", voice);
						midiOutShortMsg(handle, voice);
					}
				}

				// Wait.
				SleepTill(LastStop + p / (link ? 64 : 32));

				// Reset the variables.
				Sounds.clear();
				t = 0;

				// Show the message.
				PrintLog(BLUE, "Recall: MultiSound end.");
			}

			// Set tune.
			else if (ss.find("1=") != ss.npos)
			{
				switch (ss[2] >= 'a' ? ss[2] - 'a' + 'A' : ss[2])
				{
				case 'C':
					C = 12;
					break;
				case 'D':
					C = 14;
					break;
				case 'E':
					C = 16;
					break;
				case 'F':
					C = 17;
					break;
				case 'G':
					C = 19;
					break;
				case 'A':
					C = 9;
					break;
				case 'B':
					C = 11;
					break;
				case 'U':
					switch (ss[3] >= 'a' ? ss[3] - 'a' + 'A' : ss[3])
					{
					case 'C':
						C = 13;
						break;
					case 'D':
						C = 15;
						break;
					case 'F':
						C = 18;
						break;
					case 'G':
						C = 20;
						break;
					case 'A':
						C = 10;
						break;
					default:
						SetColor(RED);
						cout << "INPUT FILE ERROR:" << endl;
						cout << "Line " << LineNumber << ": Illegal character." << endl;
						End(1);
					}
					break;
				default:
					SetColor(RED);
					cerr << "INPUT FILE ERROR:" << endl;
					cerr << "Line " << LineNumber << ": Illegal character." << endl;
					End(1);
				}
			}

			// Set speed.
			else if (ss.find("v=") != ss.npos)
			{
				p = int(60000.0 / atof(ss.substr(2, ss.size() - 2).c_str()));
			}

			// Sound.
			else
			{
				// Variables.
				int freq = 0;		// The sound.

				// Not a stop.
				if (ss[0] != 's')
				{
					// Variables.
					size_t i;
					int h = 0;
					bool u = false;

					// h/l/u mark.
					for (i = 0; i < ss.size(); i++)
					{
						if (ss[i] >= '0' && ss[i] <= '9')
							break;
						else if (ss[i] == 'h')
							h++;
						else if (ss[i] == 'l')
							h--;
						else if (ss[i] == 'u')
							u = true;
						else
						{
							SetColor(RED);
							cout << "\rINPUT FILE ERROR:" << endl;
							cout << "Line " << LineNumber << ": Illegal character." << endl;
							End(1);
							return 0;
						}
					}

					// sound mark.
					if (ss[i] == '1')
						if (!u)freq = C + 48;
						else freq = C + 49;
					else if (ss[i] == '2')
						if (!u)freq = C + 50;
						else freq = C + 51;
					else if (ss[i] == '3')
						freq = C + 52;
					else if (ss[i] == '4')
						if (!u)freq = C + 53;
						else freq = C + 54;
					else if (ss[i] == '5')
						if (!u)freq = C + 55;
						else freq = C + 56;
					else if (ss[i] == '6')
						if (!u)freq = C + 57;
						else freq = C + 58;
					else freq = C + 59;

					// Add h/l/u result to sound.
					freq += 12 * h;

					ss.erase(0, size_t(i) + 1);
				}
				// Quiet.
				else
					ss.erase(0, 1);

				// Time.
				if (ss.size() == 0)
				{
					t = 1;
				}
				else
				{
					for (size_t i = 0; i < ss.size(); i++)
					{
						if (isdigit(ss[i]))
						{
							t += ss[i] - '0';
						}
						else if (ss[i] == 'h')
							t += 0.5;
						else if (ss[i] == 'q')
							t += 0.25;
						else if (ss[i] == 'e')
							t += 0.125;
						else
							t += 1.0 / (ss[i] - 'a' + 1);
					}
				}
				t *= p;

				// A member of MultiSound: Add to list.
				if (MultiSound)
				{
					Sounds.push_back(freq);
				}
				// Not a member of MultiSound: Play.
				else
				{
					if (freq != 0)
					{
						// Start.
						int voice = (int((quiet ? 0.5 : 1.0) * strength) << 16) + (freq << 8) + 0x90 + (Index == 0 ? 0 : Index - 1);
						PrintLog(RED | GREEN, "Recall: START (MIDI: %.8X)", voice);
						midiOutShortMsg(handle, voice);

						// Wait.
						//SleepTill(LastStop + t - double(p) / (link ? 64 : 32));
						SleepTill(LastStop + t);

						// Stop.
						if (!NoStop)
						{
							voice = (freq << 8) + 0x80 + (Index == 0 ? 0 : Index - 1);
							PrintLog(RED | GREEN, "Recall: STOP  (MIDI: %.8X)", voice);
							midiOutShortMsg(handle, voice);
						}

						// Wait.
						//SleepTill(LastStop + double(p) / (link ? 64 : 32));
					}
					else
					{
						SleepTill(LastStop + t);
					}

					// Reset the variable.
					t = 0;

				}
			}
		}

		// Wait for input if not reading from a file.
		if (!File)
		{
			SetColor();
			printf_s(">");
		}

	}
	// End of the Thread.
	PrintLog(GREEN, "Recall: Thread end.");
	return 0;
}

//Displays help information and exit.
static void help()
{
	SetColor();
	printf_s("Generate wave file according to the code file given.\n");
	printf_s("\n");
	printf_s("usage: music [options] [file]\n");
	printf_s("\n");
	printf_s("[options]: \n");
	printf_s("  -c     Do not use colorful output.\n");
	printf_s("  -o     Do not show log. (Better Sound performance!)\n");
	printf_s("  -h     Displays this help.\n");
	printf_s("  -?     Displays this help.\n");
	printf_s("[file]:  Code file, ended with \".txt\" or \".mcf\". Do not give this if you want to enter interact mode.\n");
	printf_s("\n");
	End(0);

}

//The main function.
int main(int argc, char** argv)
{
	StartTime = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();

	//Displays name and version.
	SetColor();
	printf_s("MIDIMusic, version: %s.\n", Version);
	printf_s("apple-ba (c) 2024.\n\n");
	printf_s("Lowest code file version supported: %d,\n", LowestSupported);
	printf_s("Highest code file version supported: %d.\n\n", HighestSupported);
	printf_s("%.10lld Program Initialized. \n", StartTime);

	//The arguments.
	int FilenameIndex = -1;
	bool DisplayWarning = false;
	for (int ic = 1; ic < argc; ic++)
	{
		//Options.
		if (argv[ic][0] == '-' || argv[ic][0] == '/')
		{
			switch (argv[ic][1])
			{
			case 'c':
				NoColor = true;
				break;
			case 'o':
				ShowLog = false;
				break;
			case '?':
			case 'h':
				help();
				break;
			default:
				break;
			}
		}
		else
		{
			if (FilenameIndex == -1)
			{
				FilenameIndex = ic;
			}
			else
			{
				if (!DisplayWarning)
				{
					SetColor(RED | GREEN);
					printf_s("SYSTEM WARNING: More than one files are given. Only the first one will be played.\n");
					SetColor();
				}
			}
		}

	}


	//No input file.
	if (FilenameIndex == -1)
	{
		File = false;
		SetColor();
		printf_s("No input file given. Use `MIDIMusic /?` for help.\n");
		printf_s("[Interact Mode] Type your Music code here, press Enter to play.\n");
		printf_s("Press Ctrl + Z and Enter to exit safely.\n");
	}
	//Use input file.
	else
	{
		File = true;
		InputFile = argv[FilenameIndex];
		//Check the input file name.
		if (string(argv[FilenameIndex]).size() < 4 || string(argv[FilenameIndex])[(string(argv[FilenameIndex])).size() - 4] != '.')
		{
			SetColor(RED);
			printf_s("SYSTEM ERROR: Illegal file name.\n");
			End(1);
		}

		//Try to open the input file.
		while (true)
		{
			in.open(argv[FilenameIndex]);
			if (!in.is_open())
			{
				char errstr[1024];
				strerror_s(errstr, errno);
				SetColor(RED);
				printf_s("SYSTEM ERROR: Cannot open input file %s: %s\n", argv[FilenameIndex], errstr);
				SetColor(GREEN | BLUE);
				printf_s("Retry? [Y/N]");
				SetColor();
				char c;
				cin >> c;
				if (c == 'Y' || c == 'y')continue;
				else if (c == 'N' || c == 'n')
				{
					End(1);
				}

			}
			else break;
		}

		//String inputted.
		string s;

		//Check code file version.
		bool good = false;
		int ver = -1;
		bool found = false;
		while (getline(in, s))
		{
			if (s.find("Music code file Version ") != s.npos)
			{
				stringstream ss;
				ss << s;
				ss >> s >> s >> s >> s;
				ss >> ver;
				if (ver >= LowestSupported && ver <= HighestSupported)
				{
					good = true;
				}
				if (found)
				{
					break;
				}
				found = true;
			}
			if (s.find("Thread=") != s.npos)
			{
				threads = atoi(s.substr(s.find("=") + 1).c_str());
				if (found)
				{
					break;
				}
				found = true;
			}
		}
		if (!good)
		{
			SetColor(RED);
			printf_s("INPUT FILE ERROR: Unsupported code file.\n");
			if (ver != -1)
			{
				printf_s("Input file version: %d\n", ver);
				SetColor();
				printf_s("Lowest code file version supported: %d,\n", LowestSupported);
				printf_s("Highest code file version supported: %d.\n", HighestSupported);
			}
			else printf_s("Input file version unknown, maybe your code file is too old.\n");
			End(1);
		}
	}

	// Open the MIDI stream.
	midiOutOpen(&handle, 0, 0, 0, CALLBACK_NULL);

	// Start the thread(s).
	if (threads == 0)
	{
		DWORD threadid;
		HANDLE thread;
		thread = CreateThread(NULL, 0, Play, 0, 0, &threadid);
		if (thread == 0)
		{
			SetColor(RED);
			printf_s("RUNTIME ERROR: Fail to start a thread.\nYou can restart the MIDIMusic.\n");
			End(1);
		}
		else
		{
			WaitForSingleObject(thread, INFINITE);
		}
	}
	else
	{
		int lp[MAX_THREADS] = {};
		HANDLE threadIDs[MAX_THREADS] = {};
		for (int i = 0; i < threads; i++)
		{
			lp[i] = i + 1;
			DWORD threadid;
			threadIDs[i] = CreateThread(NULL, 0, Play, &lp[i], 0, &threadid);
		}
		WaitForMultipleObjects(threads, threadIDs, true, INFINITE);
	}

	// Close the MIDI thread.
	midiOutClose(handle);

	// End.
	End(0);
}
